UserType - Advanced

Last tutorial, we discovered the UserType class to give a more object oriented vibe to our scripting environments. However, we missed on some other functionalities that can be used next to them. In this tutorial, we will see :

Using these tools, the way to personalize our user types will take a new dimension !

Through this tutorial we will see many code snippets and witness their effects through the log of messages. To ease the reading, we will introduce specific blocks, to hold the log for :

They will take the form of :

This is a message from C++

This is a message from Lua

Using this, we will avoid redundant images showing consoles in order to make the tutorial easier to read.

Context

To put some context, we take back the last tutorial's code from where it was. Some reminders about the structure and the type creation :

class Data { public : std::string _label = "I" ; int _i = 0 ; int _array [3] = {5, 4, 3} ; } ;

For this tutorial, we added an internal array of 3 integers. This will be useful later.
Else, our C++ structure is only made by the fields from last tutorial. Then, our type was declared in the environment using :

nkScripts::UserType* type = env->setUserType("nkTutorial::Data") ;

For later demonstration, we also add two instances within the environment :

Data data0 ; env->setObject("data0", "nkTutorial::Data", &data0) ; Data data1 ; env->setObject("data1", "nkTutorial::Data", &data1) ;

With this in mind, let's discover new functionalities !

Overriding built-in functions and operators

Like in C++, user types give the ability to override operators, and to some extent, some built-in functions. Let's demonstrate what it's capable of with some code, by using overrideBuiltInFunction :

nkScripts::Function* func = type->overrideBuiltInFunction(nkScripts::TYPE_BUILT_IN_FUNCTIONS::BUILT_IN_EQ) ; func->setFunction ( [] (const nkScripts::DataStack& stack) -> nkScripts::OutputValue { // Two operands for this operation, left and right Data* data0 = (Data*)stack[0]._valUser._userData ; Data* data1 = (Data*)stack[1]._valUser._userData ; return nkScripts::OutputValue(data0->_label == data1->_label && data0->_i == data1->_i) ; } ) ;

This function will take the type of built-in function / operator to overload. There are many available, from unary minus to string conversion. Best is to discover them through the API documentation of the enumeration class.

This function call returns a Function that we can populate. The function is already preset, with the parameters as being the operands involved, from left to right. For instance, in our case, first operand and second operand are fit within the data stack at indices 0 and 1. As such, what's left is to fill the function callback.

Our callback lambda just compares some fields and return the boolean result of that operation. Now, we can use it in our script :

print(data0 == data1)
true

The use of the "==" operator has been translated into a call to our now overloaded function. As simple as that !
To push further, remember the print method we had to declare last tutorial ? We can make it in a different fashion using this function :

func = type->overrideBuiltInFunction(nkScripts::TYPE_BUILT_IN_FUNCTIONS::BUILT_IN_TO_STRING) ; func->setFunction ( [] (const nkScripts::DataStack& stack) -> nkScripts::OutputValue { // This operation has only one operand, the object to stringify Data* data = (Data*)stack[0]._valUser._userData ; std::string result = data->_label + " : " + std::to_string(data->_i) ; result+= " - " + std::to_string(data->_array[0]) + ", " + std::to_string(data->_array[1]) + ", " + std::to_string(data->_array[2]) ; return nkScripts::OutputValue(result.c_str()) ; } ) ;

We override the BUILT_IN_TO_STRING function this time, having only one operand, the object to convert. Doing this allows us to update our script to :

print(data0)
I : 0 - 5, 4, 3

Overriding operators allows to align better with how they're used in C++, or help with using them within scripts as it can feel more natural.

Keep in mind some limits currently : operators take operands of the same type. It is currently not possible to mix a vector and a scalar for instance. You would need to make the scalar a vector with all members equal, and then you would be able to use it through this mechanism.

Accessing members as fields

Accessing members of a class is usually done through a getter and a setter, which could be created through the method declaration functions. However, another elegant way of accessing public members is by declaring them as fields. To do so, we need a new include :

#include <NilkinsScripts/Environments/UserTypes/UserTypeFieldDescriptor.h>

This descriptor class is used like this :

nkScripts::UserTypeFieldDescriptor fieldDesc ; fieldDesc._fieldName = "_i" ; fieldDesc._fieldType = nkScripts::FUNCTION_PARAMETER_TYPE::INT ; fieldDesc._getter = [] (void* data) -> nkScripts::OutputValue { // Cast the type to be able to use it Data* dataCast = (Data*)data ; return nkScripts::OutputValue(dataCast->_i) ; } ; fieldDesc._setter = [] (const nkScripts::DataStack& stack) -> void { // Like a method, we get the user data and then the value to assign Data* data = (Data*)stack[0]._valUser._userData ; data->_i = stack[1]._valInt ; // We do not expect any output this time, only a set } ; type->addField(fieldDesc) ;

The descriptor has first to be created. It needs the name of the field to declare, along with its type, to drive the way the parameter stacks are type checked and filled.

Then, we can choose to use or not a getter and a setter. This depends on what operations you need to expose to the environment :

In our case, we simply change the values of our _i field. Now, to use it within a script :

data0._i = 50 -- Uses the setter print(data0._i) -- Uses the getter print(data0)
50
I : 50 - 5, 4, 3

Using the fields, we are able to change the way data can be accessed or written easily.

Using types as arrays

One last thing not exposed up til now is the way to index user types as arrays. You can see it like overloading the operator [] for our types. We need to include :

#include <NilkinsScripts/Environments/UserTypes/ArrayAccessorDescriptor.h>

Then, like the field enabling function, we need to provide a descriptor of our access pattern :

nkScripts::ArrayAccessorDescriptor accessDesc ; accessDesc._fieldType = nkScripts::FUNCTION_PARAMETER_TYPE::INT ; accessDesc._readFunc = [] (const nkScripts::DataStack& stack) -> nkScripts::OutputValue { // First slot is data, second is index Data* data = (Data*)stack[0]._valUser._userData ; int index = stack[1]._valInt ; return nkScripts::OutputValue(data->_array[index]) ; } ; accessDesc._writeFunc = [] (const nkScripts::DataStack& stack) -> void { // First slot is data, second index, third is the value to write Data* data = (Data*)stack[0]._valUser._userData ; int index = stack[1]._valInt ; int value = stack[2]._valInt ; data->_array[index] = value ; } ; type->enableArrayIndexing(accessDesc) ;

First thing is to declare which type of variable we will expose through the array indexing. Here, we will expose integers.
We then give the callbacks to read / write. As with the fields, they can be provided or not, depending on the access you want to provide.
The array reading function takes a stack, that will be prefilled with the user data (0) and the index (1) requested. It is best to return the right type of data declared, here an integer.
To write, the function provides the user data (0), the index (1), and the value to assign (2). It doesn't take any return value.

Finally, we enable array indexing on the type itself using the descriptor. Then we can use it within the scripting environment :

data0[0] = 55 -- Uses the setter data0[1] = 25 print(data0[0]) -- Uses the getter print(data0)
55
I : 50 - 55, 25, 3

The type is directly used as if it was an array, using the [] operator. This descriptor providing read and write functions, we can use both of them in the script.

Conclusion

To conclude, we discovered new ways of using the UserType class to :

All of these can be used to make scripts more intuitive to write.

And with this tutorial, we covered all available functionalities of the UserType class. Use them wisely !